﻿/*
 * Copyright 2011 Edward L. Stauff.                  Contact: <EdStauff@gmail.com>.
 * 
 * This file, "EnumParser.cs", is part of the "Stauffware.Common" class library.
 * "Stauffware.Common" is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser Public License as published by the 
 * Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common" is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * the "Stauffware.Common" sources.  If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Collections.Generic;
using System.Text;

/*
 *	Possible future enhancements:
 *		allow alternate text-to-value mappings
 */

namespace Stauffware.Common.Core
{
	/// <summary>
	/// Options for parsing numbers.
	/// </summary>
	[Flags]
	public enum EnumParseOptions
	{
		/// <summary>
		/// No options.  Note that this implies (! ParseEntireInput).
		/// </summary>
		None = 0,

		#region ParseOptions compatibility

		/// <summary>
		/// Allow leading whitespace.  (Equal to ParseOptions.AllowLeadingWhite.)
		/// </summary>
		AllowLeadingWhite	= ParseOptions.AllowLeadingWhite,

		/// <summary>
		/// Allow trailing whitespace.  (Equal to ParseOptions.AllowTrailingWhite.)
		/// </summary>
		AllowTrailingWhite	= ParseOptions.AllowTrailingWhite,

		/// <summary>
		/// Allow inner whitespace (meaningful only for flags enums).
		/// (Equal to ParseOptions.AllowInnerWhite.)
		/// </summary>
		AllowInnerWhite		= ParseOptions.AllowInnerWhite,

		/// <summary>
		/// Equivalent to combining AllowLeadingWhite, AllowInnerWhite, and AllowTrailingWhite.
		/// (Equal to ParseOptions.AllowWhiteSpace.)
		/// </summary>
		AllowWhiteSpace		= ParseOptions.AllowWhiteSpace,

		/// <summary>
		/// Forces parsing of the entire input string.
		/// Without this option, extraneous characters following a valid enum value 
		/// will terminate the parsing, but not cause an error.
		/// (Equal to ParseOptions.ParseEntireInput.)
		/// </summary>
		ParseEntireInput	= ParseOptions.ParseEntireInput,

		#endregion

		/// <summary>
		/// Whether to ignore case.
		/// </summary>
		IgnoreCase			= 0x0100,

		/// <summary>
		/// Whether to recognize abbreviations (unambiguous initial substrings).
		/// </summary>
		/// <example>
		/// For an enum with values "Feral" and "Ferengi", valid abbreviations include
		/// "Fera" (for "Feral") and "Fere" (for "Ferengi").  "F", "Fe" and "Fer" would 
		/// not be recognized, because they're ambiguous: each is a prefix of both 
		/// "Feral" and "Ferengi".
		/// </example>
		AllowAbbreviations	= 0x0200,

		/// <summary>
		/// Whether to allow multiple values separated by "|", "+" or 
		/// CultureInfo.TextInfo.ListSeparator.  
		/// This option is only meaningful for enum types with the [Flags] attribute.
		/// </summary>
		AllowMultipleValues = 0x0400,
	}

	//#################################################################################
	/// <summary>
	/// An object that can parse strings into enum values.
	/// </summary>

	public class EnumParser
	{
		/// <summary>
		/// Enum bits reserved for EnumParseOptions.  
		/// "Derived" parse option enums must not redefine these.
		/// </summary>
		internal const int Reserved_ParseOptions_Bits = 0xFF00;

		/// <summary>
		/// What to use when there isn't an EnumParseOptions parameter.
		/// </summary>
		public const EnumParseOptions DefaultParseOptions = 
				EnumParseOptions.AllowWhiteSpace | EnumParseOptions.ParseEntireInput | 
					EnumParseOptions.IgnoreCase | EnumParseOptions.AllowMultipleValues;

		/// <summary>
		/// The parse options used by this parser.
		/// If the target enum type does not have the [Flags] attribute, then
		/// EnumParseOptions.AllowMultipleValues will not be set, even if it was
		/// set when passed to the EnumParser constructor.
		/// </summary>
		private readonly EnumParseOptions ParseOptions;

		// for flags enums
		private readonly string[] ValueSeparators = new string[] { null, "+", "|" };

	//	private readonly bool IsFlags;	// is T a flags enum?

		private readonly Type TargetType;

		private readonly PrefixDictionary<object> Values;

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructs a parser for enum values.
		/// EnumParseOptions.AllowMultipleValues will be cleared if the enum type
		/// does not have the [Flags] attribute.
		/// </summary>

		public EnumParser (Type enumType, EnumParseOptions options, System.Globalization.CultureInfo ci)
		{
			Require.Condition(enumType.IsEnum, "not an enum type: " + enumType.FullName);

			this.TargetType = enumType;

			this.ParseOptions = options;
			if (ci == null)
				ci = System.Globalization.CultureInfo.CurrentUICulture;

			// If it's not a Flags enum, there's no point to allowing multiple values.
			if (enumType.GetCustomAttribute<FlagsAttribute>(false) == null)
				this.ParseOptions &= ~EnumParseOptions.AllowMultipleValues;

			this.ValueSeparators[0] = ci.TextInfo.ListSeparator;

			// create dictionary of enum values (names)

			this.Values = new PrefixDictionary<object>(this.ParseOptions.HasFlag(EnumParseOptions.IgnoreCase));

			foreach (object o in EnumUtils.GetEnumValues(enumType))
				this.Values.Add(o.ToString(), o);
		}

		//-----------------------------------------------------------------------------

		/// <summary>
		/// Constructs a parser for enum values, using CultureInfo.CurrentUICulture.
		/// EnumParseOptions.AllowMultipleValues will be cleared if the enum type
		/// does not have the [Flags] attribute.
		/// </summary>
		public EnumParser (Type enumType, EnumParseOptions options) 
			: this(enumType, options, null) { }

		/// <summary>
		/// Constructs a parser for enum values, using DefaultParseOptions.
		/// EnumParseOptions.AllowMultipleValues will be cleared if the enum type
		/// does not have the [Flags] attribute.
		/// </summary>
		public EnumParser (Type enumType, System.Globalization.CultureInfo ci) 
			: this(enumType, DefaultParseOptions, ci) { }

		/// <summary>
		/// Constructs a parser for enum values, using DefaultParseOptions and 
		/// CultureInfo.CurrentUICulture.
		/// EnumParseOptions.AllowMultipleValues will be cleared if the enum type
		/// does not have the [Flags] attribute.
		/// </summary>
		public EnumParser (Type enumType) : this(enumType, DefaultParseOptions, null) { }

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Attempts to parse an enum value from a string.
		/// </summary>
		/// <param name="input">the string to parse</param>
		/// <returns>the result of the parse, regardless of whether it was successful</returns>

		public EnumParse Parse (string input)
		{
			EnumParse parse = new EnumParse();

			// initialize our bag of state information
			ParseProgress progress = new ParseProgress(input);

			if (this.ParseOptions.HasFlag(EnumParseOptions.AllowLeadingWhite))
				progress.SkipWhiteSpace();

			while (progress.LexGoodValue(this.Values, 
						this.ParseOptions.HasFlag(EnumParseOptions.AllowAbbreviations),
						this.ParseOptions.HasFlag(EnumParseOptions.ParseEntireInput)))
			{
				if (!this.ParseOptions.HasFlag(EnumParseOptions.AllowMultipleValues))
					break;

				if (this.ParseOptions.HasFlag(EnumParseOptions.AllowInnerWhite))
					progress.SkipWhiteSpace();

				if (!progress.LexGoodValueSeparator(this.ValueSeparators))
				{
					string sep = progress.LexAnyValueSeparator(this.ValueSeparators);
					if (sep != null)
					{
						parse._errorMessage = "inconsistent value separators";
						parse._parsedText = progress.ParsedInput;
						return parse;
					}
					break;
				}

				if (this.ParseOptions.HasFlag(EnumParseOptions.AllowInnerWhite))
					progress.SkipWhiteSpace();
			}

			if (progress.LastMatch.HasValue &&
				(progress.LastMatch == PrefixMatch.Complete || progress.LastMatch == PrefixMatch.Partial) &&
				this.ParseOptions.HasFlag(EnumParseOptions.AllowTrailingWhite))
					progress.SkipWhiteSpace();

			// NOTE: I am *NOT* confident about this last bit of logic.

			if (this.ParseOptions.HasFlag(EnumParseOptions.ParseEntireInput))
			{
				if (progress.HasUnparsedInput)
				{
					progress.Next++;	// include 1st unrecogized char in EnumParse.ParsedInput
					if (progress.ValuesFound > 0)
						parse._errorMessage = "extraneous input found after value";
					else parse._errorMessage = "invalid value";
				}
				else if (progress.LastMatch == null)
				{ }	// empty input: no value, no error
				else if (progress.LastMatch == PrefixMatch.Ambiguous)
					parse._errorMessage = "ambiguous abbreviation";
				else if (progress.LastMatch == PrefixMatch.None)
					parse._errorMessage = "invalid value";
				else parse._value = EnumUtils.IntToEnum(progress.CompositeValue, this.TargetType);
			}
			else  // !ParseEntireInput (okay to have leftover junk)
			{
				if (progress.ValuesFound > 0)
					parse._value = EnumUtils.IntToEnum(progress.CompositeValue, this.TargetType);
			}

			parse._parsedText = progress.ParsedInput;

			return parse;
		}

		//-----------------------------------------------------------------------------
		// Returns the value separator at the given offset of the given string, or
		// null if there isn't one.

		//public string GetValueSeparator (string input, int startPos)
		//{
		//    foreach (string sep in this.ValueSeparators)
		//        if (string.Compare(input, startPos, sep, 0, sep.Length, true) == 0)
		//            return sep;
		//    return null;
		//}

		//=============================================================================
		// Encapsulates all the state information we need during the parsing process.
		// It's a struct so we allocate space for it on the stack rather than hitting the heap.

		private struct ParseProgress
		{
			private readonly string Input;	// the input string
			public int Next;				// index of the next unexamined character
			public PrefixMatch? LastMatch;	// the result of our last PrefixDictionary lookup
			public ulong CompositeValue;	// the parsed enum value
			public int ValuesFound;			// the number of good enum values found so far
			public string ValueSep;			// the value separator already encountered, or null

			public bool HasUnparsedInput { get { return this.Next < this.Input.Length; } }
			public string ParsedInput { get { return this.Input.Substring(0, this.Next); } }

			//-----------------------------------------------------------------------------

			public ParseProgress (string input)
			{
				this.Input = input;
				this.Next = 0;
				this.LastMatch = null;
				this.CompositeValue = 0;
				this.ValuesFound = 0;
				this.ValueSep = null;
			}

			//-----------------------------------------------------------------------------
			// Returns whether the next token is a good enum value.  This.Next is advanced 
			// unless (a) it's not a good match AND (b) we don't have to parse the entire input.
			
			public bool LexGoodValue (PrefixDictionary<object> dict, bool allowAbbrev, bool entireInput)
			{
				int wordLen = this.LengthOfNextWord();
				if (wordLen == 0)
				{
					if (entireInput && this.HasUnparsedInput)
					{
						this.LastMatch = PrefixMatch.None;
						this.Next++;	// make first unrecognized char show up in EnumParse.ParsedText
					}
					return false;
				}

				object enumValue;
				this.LastMatch = dict.Lookup(this.SubString(wordLen), out enumValue);

				if (this.LastMatch == PrefixMatch.None || this.LastMatch == PrefixMatch.Ambiguous)
				{
					if (entireInput)
						this.Next += wordLen;	// make bad match show up in EnumParse.ParsedText
					return false;
				}

				if (this.LastMatch == PrefixMatch.Partial && !allowAbbrev)
				{
					if (entireInput)
						this.Next += wordLen;	// make bad match show up in EnumParse.ParsedText
					this.LastMatch = PrefixMatch.None;
					return false;
				}

				this.Next += wordLen;
				this.AddValue(Convert.ToUInt64(enumValue));
				return true;
			}

			//-----------------------------------------------------------------------------
			// Returns the length of the next "word" in the input string: a 
			// sequence of characters that, lexically, could be an enum value name.

			public int LengthOfNextWord ( )
			{
				int n;
				for (n = this.Next; n < this.Input.Length; n++)
					if (!(char.IsLetterOrDigit(this.Input[n]) || GoodNonAlphaNumChars.Contains(this.Input[n])))
						break;

				if (n > 0 && char.IsDigit(this.Input[0]))
					return 0;

				return n - this.Next;
			}

			private const string GoodNonAlphaNumChars = "_";

			//-----------------------------------------------------------------------------
			// Returns whether the next token is a good value separator.
			// If it is, this.Next is advanced.

			public bool LexGoodValueSeparator (IList<string> valueSeparators)
			{
				// if we've already encountered a value separator, then that's the one to look for
				if (this.ValueSep != null)
				{
					if (string.Compare(this.Input, this.Next, this.ValueSep, 0, this.ValueSep.Length, StringComparison.InvariantCultureIgnoreCase) == 0)
					{
						this.Next += this.ValueSep.Length;
						return true;
					}
				}
				else  // haven't seen a value separator yet; check for any valid one
				{
					string sep = this.LexAnyValueSeparator(valueSeparators);
					if (sep != null)
					{
						this.ValueSep = sep;
						return true;
					}
				}

				return false;	// didn't find one
			}

			//-----------------------------------------------------------------------------
			// Returns whether the next token is any value separator.
			// If it is, this.Next is advanced.

			public string LexAnyValueSeparator (IList<string> valueSeparators)
			{
				foreach (string sep in valueSeparators)
					if (string.Compare(this.Input, this.Next, sep, 0, sep.Length, StringComparison.InvariantCultureIgnoreCase) == 0)
					{
						this.Next += sep.Length;
						return sep;
					}
				return null;
			}

			//-----------------------------------------------------------------------------

			public string SubString (int length)
			{ return this.Input.Substring(this.Next, length); }

			//-----------------------------------------------------------------------------

			public void SkipWhiteSpace ()
			{
				for ( ; this.Next < this.Input.Length; this.Next++)
					if (!char.IsWhiteSpace(this.Input[this.Next]))
						break;
			}

			//-----------------------------------------------------------------------------

			public void AddValue (UInt64 newValue)
			{
				this.CompositeValue |= newValue;
				this.ValuesFound++;
			}

			//-----------------------------------------------------------------------------
		}
	}

	//#################################################################################
	/// <summary>
	/// Encapsulates the result of an attempt to parse an enum value from a string,
	/// whether successful or unsuccessful.
	/// </summary>

	public class EnumParse : ReferenceParse<object> 
	{

	}

	//#################################################################################
	/// <summary>
	/// A generic class for objects that can parse strings into enum values.
	/// </summary>

	public class EnumParser<T> : EnumParser where T : struct
	{
		/// <summary>
		/// Constructor
		/// </summary>
		public EnumParser () : base(typeof(T)) { }

		/// <summary>
		/// Constructor
		/// </summary>
		public EnumParser (EnumParseOptions opt) : base(typeof(T), opt) { }

		/// <summary>
		/// Parses a string into an EnumParse.
		/// </summary>
		public new EnumParse<T> Parse (string input)
		{
			return new EnumParse<T>(base.Parse(input));
			//EnumParse parse = base.Parse(input);
			//EnumParse<T> gParse = new EnumParse<T>();
			//gParse._value = (T) parse.Value;
			//gParse._errorMessage = parse._errorMessage;
			//return gParse;
		}
	}

	//#################################################################################
	/// <summary>
	/// Encapsulates the result of an attempt to parse an enum value from a string,
	/// whether successful or unsuccessful.
	/// </summary>

	public class EnumParse<T> : ValueParse<T> where T : struct
	{
		/// <summary>
		/// Copy constructor.
		/// </summary>
		public EnumParse (EnumParse that)
		{
			this._errorMessage = that._errorMessage;
			this._parsedText = that._parsedText;
			if (that._value != null)
				this._value = (T) that._value;
		}
	}

	//#################################################################################
}
